// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/base/clipboard/clipboard_util_win.h"

#include <shellapi.h>
#include <shlwapi.h>
#include <wininet.h> // For INTERNET_MAX_URL_LENGTH.

#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/macros.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/win/scoped_hglobal.h"
#include "net/base/filename_util.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/custom_data_helper.h"
#include "url/gurl.h"

namespace ui {

namespace {

    bool HasData(IDataObject* data_object, const Clipboard::FormatType& format)
    {
        FORMATETC format_etc = format.ToFormatEtc();
        return SUCCEEDED(data_object->QueryGetData(&format_etc));
    }

    bool GetData(IDataObject* data_object,
        const Clipboard::FormatType& format,
        STGMEDIUM* medium)
    {
        FORMATETC format_etc = format.ToFormatEtc();
        return SUCCEEDED(data_object->GetData(&format_etc, medium));
    }

    bool GetUrlFromHDrop(IDataObject* data_object,
        GURL* url,
        base::string16* title)
    {
        DCHECK(data_object && url && title);

        bool success = false;
        STGMEDIUM medium;
        if (!GetData(data_object, Clipboard::GetCFHDropFormatType(), &medium))
            return false;

        {
            base::win::ScopedHGlobal<HDROP> hdrop(medium.hGlobal);

            if (!hdrop.get())
                return false;

            wchar_t filename[MAX_PATH];
            if (DragQueryFileW(hdrop.get(), 0, filename, arraysize(filename))) {
                wchar_t url_buffer[INTERNET_MAX_URL_LENGTH];
                if (0 == _wcsicmp(PathFindExtensionW(filename), L".url") && GetPrivateProfileStringW(L"InternetShortcut", L"url", 0, url_buffer, arraysize(url_buffer), filename)) {
                    *url = GURL(url_buffer);
                    PathRemoveExtension(filename);
                    title->assign(PathFindFileName(filename));
                    success = url->is_valid();
                }
            }
        }

        ReleaseStgMedium(&medium);
        return success;
    }

    void SplitUrlAndTitle(const base::string16& str,
        GURL* url,
        base::string16* title)
    {
        DCHECK(url && title);
        size_t newline_pos = str.find('\n');
        if (newline_pos != base::string16::npos) {
            *url = GURL(base::string16(str, 0, newline_pos));
            title->assign(str, newline_pos + 1, base::string16::npos);
        } else {
            *url = GURL(str);
            title->assign(str);
        }
    }

} // namespace

bool ClipboardUtil::HasUrl(IDataObject* data_object, bool convert_filenames)
{
    DCHECK(data_object);
    return HasData(data_object, Clipboard::GetMozUrlFormatType()) || HasData(data_object, Clipboard::GetUrlWFormatType()) || HasData(data_object, Clipboard::GetUrlFormatType()) || (convert_filenames && HasFilenames(data_object));
}

bool ClipboardUtil::HasFilenames(IDataObject* data_object)
{
    DCHECK(data_object);
    return HasData(data_object, Clipboard::GetCFHDropFormatType()) || HasData(data_object, Clipboard::GetFilenameWFormatType()) || HasData(data_object, Clipboard::GetFilenameFormatType());
}

bool ClipboardUtil::HasFileContents(IDataObject* data_object)
{
    DCHECK(data_object);
    return HasData(data_object, Clipboard::GetFileContentZeroFormatType());
}

bool ClipboardUtil::HasHtml(IDataObject* data_object)
{
    DCHECK(data_object);
    return HasData(data_object, Clipboard::GetHtmlFormatType()) || HasData(data_object, Clipboard::GetTextHtmlFormatType());
}

bool ClipboardUtil::HasPlainText(IDataObject* data_object)
{
    DCHECK(data_object);
    return HasData(data_object, Clipboard::GetPlainTextWFormatType()) || HasData(data_object, Clipboard::GetPlainTextFormatType());
}

bool ClipboardUtil::GetUrl(IDataObject* data_object,
    GURL* url,
    base::string16* title,
    bool convert_filenames)
{
    DCHECK(data_object && url && title);
    if (!HasUrl(data_object, convert_filenames))
        return false;

    // Try to extract a URL from |data_object| in a variety of formats.
    STGMEDIUM store;
    if (GetUrlFromHDrop(data_object, url, title))
        return true;

    if (GetData(data_object, Clipboard::GetMozUrlFormatType(), &store) || GetData(data_object, Clipboard::GetUrlWFormatType(), &store)) {
        {
            // Mozilla URL format or unicode URL
            base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
            SplitUrlAndTitle(data.get(), url, title);
        }
        ReleaseStgMedium(&store);
        return url->is_valid();
    }

    if (GetData(data_object, Clipboard::GetUrlFormatType(), &store)) {
        {
            // URL using ascii
            base::win::ScopedHGlobal<char*> data(store.hGlobal);
            SplitUrlAndTitle(base::UTF8ToWide(data.get()), url, title);
        }
        ReleaseStgMedium(&store);
        return url->is_valid();
    }

    if (convert_filenames) {
        std::vector<base::string16> filenames;
        if (!GetFilenames(data_object, &filenames))
            return false;
        DCHECK_GT(filenames.size(), 0U);
        *url = net::FilePathToFileURL(base::FilePath(filenames[0]));
        return url->is_valid();
    }

    return false;
}

bool ClipboardUtil::GetFilenames(IDataObject* data_object,
    std::vector<base::string16>* filenames)
{
    DCHECK(data_object && filenames);
    if (!HasFilenames(data_object))
        return false;

    STGMEDIUM medium;
    if (GetData(data_object, Clipboard::GetCFHDropFormatType(), &medium)) {
        {
            base::win::ScopedHGlobal<HDROP> hdrop(medium.hGlobal);
            if (!hdrop.get())
                return false;

            const int kMaxFilenameLen = 4096;
            const unsigned num_files = DragQueryFileW(hdrop.get(), 0xffffffff, 0, 0);
            for (unsigned int i = 0; i < num_files; ++i) {
                wchar_t filename[kMaxFilenameLen];
                if (!DragQueryFileW(hdrop.get(), i, filename, kMaxFilenameLen))
                    continue;
                filenames->push_back(filename);
            }
        }
        ReleaseStgMedium(&medium);
        return true;
    }

    if (GetData(data_object, Clipboard::GetFilenameWFormatType(), &medium)) {
        {
            // filename using unicode
            base::win::ScopedHGlobal<wchar_t*> data(medium.hGlobal);
            if (data.get() && data.get()[0])
                filenames->push_back(data.get());
        }
        ReleaseStgMedium(&medium);
        return true;
    }

    if (GetData(data_object, Clipboard::GetFilenameFormatType(), &medium)) {
        {
            // filename using ascii
            base::win::ScopedHGlobal<char*> data(medium.hGlobal);
            if (data.get() && data.get()[0])
                filenames->push_back(base::SysNativeMBToWide(data.get()));
        }
        ReleaseStgMedium(&medium);
        return true;
    }

    return false;
}

bool ClipboardUtil::GetPlainText(IDataObject* data_object,
    base::string16* plain_text)
{
    DCHECK(data_object && plain_text);
    if (!HasPlainText(data_object))
        return false;

    STGMEDIUM store;
    if (GetData(data_object, Clipboard::GetPlainTextWFormatType(), &store)) {
        {
            // Unicode text
            base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
            plain_text->assign(data.get());
        }
        ReleaseStgMedium(&store);
        return true;
    }

    if (GetData(data_object, Clipboard::GetPlainTextFormatType(), &store)) {
        {
            // ascii text
            base::win::ScopedHGlobal<char*> data(store.hGlobal);
            plain_text->assign(base::UTF8ToWide(data.get()));
        }
        ReleaseStgMedium(&store);
        return true;
    }

    // If a file is dropped on the window, it does not provide either of the
    // plain text formats, so here we try to forcibly get a url.
    GURL url;
    base::string16 title;
    if (GetUrl(data_object, &url, &title, false)) {
        *plain_text = base::UTF8ToUTF16(url.spec());
        return true;
    }
    return false;
}

bool ClipboardUtil::GetHtml(IDataObject* data_object,
    base::string16* html, std::string* base_url)
{
    DCHECK(data_object && html && base_url);

    STGMEDIUM store;
    if (HasData(data_object, Clipboard::GetHtmlFormatType()) && GetData(data_object, Clipboard::GetHtmlFormatType(), &store)) {
        {
            // MS CF html
            base::win::ScopedHGlobal<char*> data(store.hGlobal);

            std::string html_utf8;
            CFHtmlToHtml(std::string(data.get(), data.Size()), &html_utf8, base_url);
            html->assign(base::UTF8ToWide(html_utf8));
        }
        ReleaseStgMedium(&store);
        return true;
    }

    if (!HasData(data_object, Clipboard::GetTextHtmlFormatType()))
        return false;

    if (!GetData(data_object, Clipboard::GetTextHtmlFormatType(), &store))
        return false;

    {
        // text/html
        base::win::ScopedHGlobal<wchar_t*> data(store.hGlobal);
        html->assign(data.get());
    }
    ReleaseStgMedium(&store);
    return true;
}

bool ClipboardUtil::GetFileContents(IDataObject* data_object,
    base::string16* filename, std::string* file_contents)
{
    DCHECK(data_object && filename && file_contents);
    if (!HasData(data_object, Clipboard::GetFileContentZeroFormatType()) && !HasData(data_object, Clipboard::GetFileDescriptorFormatType()))
        return false;

    STGMEDIUM content;
    // The call to GetData can be very slow depending on what is in
    // |data_object|.
    if (GetData(
            data_object, Clipboard::GetFileContentZeroFormatType(), &content)) {
        if (TYMED_HGLOBAL == content.tymed) {
            base::win::ScopedHGlobal<char*> data(content.hGlobal);
            file_contents->assign(data.get(), data.Size());
        }
        ReleaseStgMedium(&content);
    }

    STGMEDIUM description;
    if (GetData(data_object,
            Clipboard::GetFileDescriptorFormatType(),
            &description)) {
        {
            base::win::ScopedHGlobal<FILEGROUPDESCRIPTOR*> fgd(description.hGlobal);
            // We expect there to be at least one file in here.
            DCHECK_GE(fgd->cItems, 1u);
            filename->assign(fgd->fgd[0].cFileName);
        }
        ReleaseStgMedium(&description);
    }
    return true;
}

bool ClipboardUtil::GetWebCustomData(
    IDataObject* data_object,
    std::map<base::string16, base::string16>* custom_data)
{
    DCHECK(data_object && custom_data);

    if (!HasData(data_object, Clipboard::GetWebCustomDataFormatType()))
        return false;

    STGMEDIUM store;
    if (GetData(data_object, Clipboard::GetWebCustomDataFormatType(), &store)) {
        {
            base::win::ScopedHGlobal<char*> data(store.hGlobal);
            ReadCustomDataIntoMap(data.get(), data.Size(), custom_data);
        }
        ReleaseStgMedium(&store);
        return true;
    }
    return false;
}

// HtmlToCFHtml and CFHtmlToHtml are based on similar methods in
// WebCore/platform/win/ClipboardUtilitiesWin.cpp.
/*
 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// Helper method for converting from text/html to MS CF_HTML.
// Documentation for the CF_HTML format is available at
// http://msdn.microsoft.com/en-us/library/aa767917(VS.85).aspx
std::string ClipboardUtil::HtmlToCFHtml(const std::string& html,
    const std::string& base_url)
{
    if (html.empty())
        return std::string();

#define MAX_DIGITS 10
#define MAKE_NUMBER_FORMAT_1(digits) MAKE_NUMBER_FORMAT_2(digits)
#define MAKE_NUMBER_FORMAT_2(digits) "%0" #digits "u"
#define NUMBER_FORMAT MAKE_NUMBER_FORMAT_1(MAX_DIGITS)

    static const char* header = "Version:0.9\r\n"
                                "StartHTML:" NUMBER_FORMAT "\r\n"
                                "EndHTML:" NUMBER_FORMAT "\r\n"
                                "StartFragment:" NUMBER_FORMAT "\r\n"
                                "EndFragment:" NUMBER_FORMAT "\r\n";
    static const char* source_url_prefix = "SourceURL:";

    static const char* start_markup = "<html>\r\n<body>\r\n<!--StartFragment-->";
    static const char* end_markup = "<!--EndFragment-->\r\n</body>\r\n</html>";

    // Calculate offsets
    size_t start_html_offset = strlen(header) - strlen(NUMBER_FORMAT) * 4 + MAX_DIGITS * 4;
    if (!base_url.empty()) {
        start_html_offset += strlen(source_url_prefix) + base_url.length() + 2; // Add 2 for \r\n.
    }
    size_t start_fragment_offset = start_html_offset + strlen(start_markup);
    size_t end_fragment_offset = start_fragment_offset + html.length();
    size_t end_html_offset = end_fragment_offset + strlen(end_markup);

    std::string result = base::StringPrintf(header,
        start_html_offset,
        end_html_offset,
        start_fragment_offset,
        end_fragment_offset);
    if (!base_url.empty()) {
        result.append(source_url_prefix);
        result.append(base_url);
        result.append("\r\n");
    }
    result.append(start_markup);
    result.append(html);
    result.append(end_markup);

#undef MAX_DIGITS
#undef MAKE_NUMBER_FORMAT_1
#undef MAKE_NUMBER_FORMAT_2
#undef NUMBER_FORMAT

    return result;
}

// Helper method for converting from MS CF_HTML to text/html.
void ClipboardUtil::CFHtmlToHtml(const std::string& cf_html,
    std::string* html,
    std::string* base_url)
{
    size_t fragment_start = std::string::npos;
    size_t fragment_end = std::string::npos;

    ClipboardUtil::CFHtmlExtractMetadata(
        cf_html, base_url, NULL, &fragment_start, &fragment_end);

    if (html && fragment_start != std::string::npos && fragment_end != std::string::npos) {
        *html = cf_html.substr(fragment_start, fragment_end - fragment_start);
        base::TrimWhitespaceASCII(*html, base::TRIM_ALL, html);
    }
}

void ClipboardUtil::CFHtmlExtractMetadata(const std::string& cf_html,
    std::string* base_url,
    size_t* html_start,
    size_t* fragment_start,
    size_t* fragment_end)
{
    // Obtain base_url if present.
    if (base_url) {
        static std::string src_url_str("SourceURL:");
        size_t line_start = cf_html.find(src_url_str);
        if (line_start != std::string::npos) {
            size_t src_end = cf_html.find("\n", line_start);
            size_t src_start = line_start + src_url_str.length();
            if (src_end != std::string::npos && src_start != std::string::npos) {
                *base_url = cf_html.substr(src_start, src_end - src_start);
                base::TrimWhitespaceASCII(*base_url, base::TRIM_ALL, base_url);
            }
        }
    }

    // Find the markup between "<!--StartFragment-->" and "<!--EndFragment-->".
    // If the comments cannot be found, like copying from OpenOffice Writer,
    // we simply fall back to using StartFragment/EndFragment bytecount values
    // to determine the fragment indexes.
    std::string cf_html_lower = base::ToLowerASCII(cf_html);
    size_t markup_start = cf_html_lower.find("<html", 0);
    if (html_start) {
        *html_start = markup_start;
    }
    size_t tag_start = cf_html.find("<!--StartFragment", markup_start);
    if (tag_start == std::string::npos) {
        static std::string start_fragment_str("StartFragment:");
        size_t start_fragment_start = cf_html.find(start_fragment_str);
        if (start_fragment_start != std::string::npos) {
            *fragment_start = static_cast<size_t>(atoi(cf_html.c_str() + start_fragment_start + start_fragment_str.length()));
        }

        static std::string end_fragment_str("EndFragment:");
        size_t end_fragment_start = cf_html.find(end_fragment_str);
        if (end_fragment_start != std::string::npos) {
            *fragment_end = static_cast<size_t>(atoi(cf_html.c_str() + end_fragment_start + end_fragment_str.length()));
        }
    } else {
        *fragment_start = cf_html.find('>', tag_start) + 1;
        size_t tag_end = cf_html.rfind("<!--EndFragment", std::string::npos);
        *fragment_end = cf_html.rfind('<', tag_end);
    }
}

} // namespace ui
